如何设计一个类似Twitter的社交网络服务?
点击关注公众号,AI,编程干货及时送达
让我们设计一个类似Twitter的社交网络服务。服务的用户可以发布推文,关注其他人,并收藏推文。
难度级别:中等
1. 什么是Twitter?
Twitter是一个在线社交网络服务,用户在上面发布和阅读称为"推文"的短篇140字信息。已注册用户可以发布和阅读推文,但未注册用户只能阅读。用户可以通过网页界面、短信或移动应用访问Twitter。
2. 系统的需求和目标
我们将设计一个更简单版本的Twitter,具有以下需求:
功能需求
1. 用户应能够发布新推文。
2. 用户应能够关注其他用户。
3. 用户应能够将推文标记为收藏。
4. 服务应能够创建和显示用户的时间线,包括用户关注的所有人的顶部推文。
5. 推文可以包含照片和视频。
非功能需求
1. 我们的服务需要高可用性。
2. 系统的可接受延迟为200毫秒,用于时间线生成。
3. 为了可用性,一致性可以降低;如果用户一段时间内看不到推文,这应该是可以接受的。
扩展需求
1. 搜索推文。
2. 回复推文。
3. 热门话题 - 当前热门话题/搜索。
4. 标记其他用户。
5. 推文通知。
6. 推荐关注谁?
7. 精彩时刻。
3. 容量估计和约束
假设我们有10亿的总用户,每日活跃用户(DAU)有2亿。也假设我们每天有1亿条新推文,每个用户平均关注200人。
每天有多少收藏?如果每个用户每天平均收藏5条推文,我们将有:
2亿用户 * 5收藏 => 10亿收藏
我们的系统会生成多少推文浏览量?假设平均每个用户每天访问他们的时间线两次,并访问五个其他人的页面。在每个页面上,如果用户看到20条推文,那么我们的系统将每天生成280亿的总推文浏览量:
2亿DAU * ((2 + 5) * 20推文) => 280亿/天
存储估计假设每条推文有140个字符,我们需要两个字节来存储一个字符(无压缩)。假设我们需要30字节来存储每条推文的元数据(如ID,时间戳,用户ID等)。我们需要的总存储量为:
1亿 * (280 + 30)字节 => 30GB/天
我们五年的存储需求是多少?我们需要多少存储空间来存储用户数据、关注、收藏?我们将这个问题留给练习。
并非所有的推文都会有媒体,假设平均每五条推文有一张照片,每十条推文有一个视频。也假设平均一张照片是200KB,一个视频是2MB。这将导致我们每天有24TB的新媒体。
(1亿/5照片 * 200KB) + (1亿/10视频 * 2MB) ~= 24TB/天
带宽估计由于总入口是每天24TB,这将转换为290MB/秒。
请记住,我们每天有280亿条推文浏览量。我们必须显示每条推文的照片(如果有照片),但假设用户在他们的时间线上看每三个视频就看一次。所以,总出口将是:
(280亿 * 280字节) / 86400秒的文本 => 93MB/s
+ (280亿/5 * 200KB ) / 86400秒的照片 => 13GB/S
+ (280亿/10/3 * 2MB ) / 86400秒的视频 => 22GB/s
总计 ~= 35GB/s
4. 系统API
💡 一旦我们确定了需求,定义系统API总是一个好主意。这应明确地表明系统的期望。 我们可以有SOAP或REST API来暴露我们的服务功能。 下面可能是发布新推文的API定义:
参数: api_dev_key(字符串):注册账户的API开发者密钥。这将被用于,包括但不限于,根据用户分配的配额进行限流。 tweet_data(字符串):推文的文本,通常最多140个字符。 tweet_location(字符串):此推文所指的可选位置(经度,纬度)。 user_location(字符串):添加推文的用户的可选位置(经度,纬度)。 media_ids(数字数组):与推文关联的媒体id的可选列表。(所有媒体照片、视频等需要单独上传)。
返回:(字符串) 成功发布的推文将返回访问该推文的URL。否则,返回适当的HTTP错误。
5. 高级系统设计
我们需要一个系统,可以高效地存储所有新的推文,100M/86400s => 每秒1150条推文,并读取28B/86400s => 每秒325K条推文。从需求中很明显,这将是一个读取重的系统。
在高层次上,我们需要多个应用服务器来服务所有这些请求,前面有负载均衡器进行流量分配。在后端,我们需要一个有效的数据库,可以存储所有新的推文并支持大量的读取。我们还需要一些文件存储来存储照片和视频。
虽然我们预计的日写入负载是1亿,读取负载是280亿推文。这意味着平均我们的系统每秒将接收约1160条新推文和325K读取请求。然而,这些流量在一天中的分布是不均匀的,虽然在高峰时期我们应该预期至少有几千次写请求和每秒约1M的读取请求。在设计我们的系统架构时,我们应该记住这一点。
6. 数据库模式
我们需要存储有关用户、他们的推文、他们喜欢的推文以及他们关注的人的数据。
对于在上述模式中选择使用SQL还是NoSQL数据库来存储数据,请参阅设计Instagram下的'数据库模式'。
7. 数据分片
由于我们每天有大量的新推文,我们的读取负载也非常高,我们需要将我们的数据分布到多台机器上,这样我们就可以高效地读取/写入。我们有许多选项来分片我们的数据;让我们逐一讨论:
基于UserID的分片:我们可以尝试将一个用户的所有数据存储在一台服务器上。在存储时,我们可以将UserID传递给我们的哈希函数,该函数将用户映射到一个数据库服务器,在那里我们将存储用户的所有推文、收藏、关注等。在查询用户的推文/关注/收藏时,我们可以询问我们的哈希函数在哪里可以找到用户的数据,然后从那里读取。这种方法有几个问题:
1. 如果一个用户变得非常热门怎么办?可能会有很多查询在服务器上执行。这种高负载将影响我们的服务性能。
2. 随着时间的推移,有些用户可能会存储很多推文或有很多关注者。保持用户数据增长的均匀分布是相当困难的。
要从这些情况中恢复过来,我们必须重新分区/重新分布我们的数据,或者使用一致性哈希。
基于TweetID的分片:我们的哈希函数将每个TweetID映射到一个随机服务器,我们将在那里存储该推文。要搜索推文,我们必须查询所有服务器,每个服务器都将返回一组推文。一个集中的服务器将聚合这些结果,返回给用户。让我们看一下时间线生成的例子;以下是我们的系统为了生成用户的时间线需要执行的步骤数量:
1. 我们的应用(app)服务器将找到用户关注的所有人。
2. 应用服务器将向所有数据库服务器发送查询,以找到这些人的推文。
3. 每个数据库服务器都会找到每个用户的推文,按照最近的顺序排序,并返回顶部的推文。
4. 应用服务器将合并所有结果,并再次排序,返回顶部的结果给用户。
这种方法解决了热门用户的问题,但与按UserID分片相比,我们必须查询所有数据库分区才能找到用户的推文,这可能导致更高的延迟。
通过引入缓存来存储热门推文在数据库服务器前面,我们可以进一步提高我们的性能。
基于推文创建时间的分片:基于创建时间存储推文将使我们能够快速获取所有顶部的推文,我们只需要查询一小部分服务器。这里的问题是,流量负载不会分布,例如,在写入时,所有新的推文都会到达一个服务器,其余的服务器会处于空闲状态。同样,读取时,持有最新数据的服务器相比持有旧数据的服务器会有很高的负载。
如果我们可以结合TweetID和Tweet创建时间的分片呢?如果我们不单独存储推文创建时间,并使用TweetID来反映,我们可以获得两种方法的好处。这样,找到最新的推文将非常快。为此,我们必须使我们的系统中的每个TweetID都是全球唯一的,并且每个TweetID也应包含时间戳。
我们可以使用纪元时间来实现这一点。比如说我们的TweetID有两部分:第一部分将表示纪元秒,第二部分将是自增序列。所以,要制作一个新的TweetID,我们可以取当前的纪元时间,并附加一个自增数。我们可以从这个TweetID中找出分片号,并存储在那里。
我们的TweetID可能有多大?假设我们的纪元时间从今天开始,我们需要多少位来存储未来50年的秒数?
86400 sec/day * 365(每年的天数)* 50(年)=> 1.6B
我们需要31位来存储这个数字。由于我们平均每秒预期有1150个新的推文,我们可以分配17位来存储自增序列;
这将使我们的TweetID(推文ID)长度达到48位。因此,我们每秒可以存储 (2^17 =>130K) 个新的推文。我们可以每秒重置我们的自增序列。为了容错和更好的性能,我们可以有两个数据库服务器为我们生成自增键,一个生成偶数键,另一个生成奇数键。
如果我们假设我们当前的纪元秒是“1483228800”,我们的TweetID(推文ID)将如下所示:
1483228800 000001
1483228800 000002
1483228800 000003
1483228800 000004
如果我们将我们的TweetID(推文ID)长度设为64位(8字节),我们可以轻松地存储未来100年的推文,并且可以以毫秒粒度进行存储。
在上述方法中,我们仍然需要查询所有的服务器来生成时间线,但我们的读取(和写入)将会显著地快很多。
1. 由于我们没有任何二级索引(基于创建时间),这将减少我们的写入延迟。
2. 在读取时,我们不需要在创建时间上进行筛选,因为我们的主键已经包含了纪元时间。
8. 缓存
我们可以为数据库服务器引入一个缓存,用于缓存热门推文和用户。我们可以使用像Memcache(内存缓存)这样的现成解决方案,它可以存储整个推文对象。在请求数据库之前,应用服务器可以快速检查缓存中是否有所需的推文。根据客户的使用模式,我们可以确定我们需要多少缓存服务器。
哪种缓存替换策略最符合我们的需求?当缓存满了,我们想用一个更新/更热门的推文替换一个推文,我们应该怎么选择?最近最少使用(LRU)可能是我们系统的一个合理策略。根据这个策略,我们首先丢弃最近最少查看的推文。
我们如何拥有一个更智能的缓存?如果我们遵循80-20规则,即20%的推文产生80%的读取流量,这意味着某些推文非常受欢迎,大多数人都会阅读它们。这表明我们可以尝试缓存每个分片每日读取量的20%。
如果我们缓存最新的数据会怎么样?我们的服务可以从这种方法中受益。比如,如果我们80%的用户只查看过去三天的推文;我们可以尝试缓存过去三天的所有推文。假设我们有专门的缓存服务器,可以缓存过去三天所有用户的所有推文。如上面估计的,我们每天会收到1亿条新的推文,或者说30GB的新数据(不包括照片和视频)。如果我们想要存储过去三天的所有推文,我们将需要不到100GB的内存。这些数据可以轻易地放入一个服务器,但
我们应该将其复制到多个服务器上,将所有的读取流量分配出去,以减轻缓存服务器的负载。所以,每当我们生成用户的时间线时,我们可以询问缓存服务器是否有该用户的所有最近推文。如果有,我们可以直接从缓存中返回所有的数据。如果缓存中的推文不够,我们必须查询后端服务器来获取那些数据。在类似的设计中,我们可以尝试缓存过去三天的照片和视频。
我们的缓存将像一个哈希表,其中‘key’将是‘OwnerID(所有者ID)’,‘value’将是一个双向链表,包含了过去三天该用户的所有推文。由于我们希望先获取最新的数据,我们可以总是在链表的头部插入新的推文,这意味着所有的旧推文都会靠近链表的尾部。因此,我们可以从尾部移除推文,为新的推文腾出空间。
9. 时间线生成
有关时间线生成的详细讨论,请参阅"设计Facebook的新闻动态(Designing Facebook’s Newsfeed)"。
10. 复制和容错
由于我们的系统是读取密集型的,我们可以为每个数据库分区设置多个次级数据库服务器。次级服务器仅用于读取流量。所有的写入操作都将首先传输到主服务器,然后复制到次级服务器。这个方案也将给我们提供容错性,因为每当主服务器出现故障时,我们可以切换到次级服务器。
11. 负载均衡
我们可以在系统的三个地方添加负载均衡层 1) 客户端和应用服务器之间 2) 应用服务器和数据库复制服务器之间 3) 聚合服务器和缓存服务器之间。初始阶段,可以采取简单的轮询方法;这种方法将传入请求平均分配到各个服务器。这种负载均衡易于实现,且不会引入任何额外开销。这种方法的另一个优点是,如果服务器宕机,负载均衡器会将其从轮询中剔除,并停止向其发送任何流量。轮询负载均衡的问题在于,它不会考虑到服务器的负载。如果服务器过载或速度慢,负载均衡器不会停止向该服务器发送新请求。为了处理这个问题,可以采用更智能的负载均衡解决方案,该解决方案会定期查询后端服务器的负载情况,并根据这些信息调整流量。
12. 监控
监控我们的系统的能力是至关重要的。我们应该持续收集数据,以便即时了解我们的系统运行情况。我们可以收集以下指标/计数器,以理解我们的服务性能:
1. 每天/每秒新增推文数量,每日峰值是多少?
2. 时间线交付统计,我们的服务每天/每秒交付多少推文。
3. 用户刷新时间线所看到的平均延迟。
通过监控这些计数器,我们会知道是否需要更多的复制、负载均衡或缓存。
13. 扩展需求
我们如何提供feeds?获取某人关注的人的所有最新推文,然后按时间合并/排序它们。使用分页来获取/显示推文。只从某人关注的所有人那里获取前N条推文。这个N将取决于客户端的视口,因为在移动设备上,我们显示的推文数量比Web客户端少。我们还可以缓存下一批热门推文以加快速度。
另外,我们可以预先生成feed以提高效率;详情请参见“Designing Instagram(设计Instagram)”下的“排名和时间线生成”。
转推(Retweet):对于数据库中的每个推文对象,我们可以存储原始推文的ID,而不在此转推对象上存储任何内容。
热门话题:我们可以缓存在过去N秒内最频繁出现的标签或搜索查询,并在每M秒后更新它们。我们可以根据推文、搜索查询、转推或点赞的频率对热门话题进行排名。我们可以给更多人看到的话题赋予更高的权重。
推荐关注谁?如何给出建议?这个功能将提高用户参与度。我们可以推荐某人关注的人的朋友。我们可以下探两到三层,找到更多的名人供建议。我们可以优先推荐关注者更多的人。
由于一次只能给出几个建议,使用机器学习(ML)来洗牌和重新排序。ML信号可能包括近期增加了关注者的人,如果其他人关注了这个用户,共有的关注者,共有的位置或兴趣等。
瞬间(Moments):获取不同网站过去1或2小时的头条新闻,找出相关推文,对它们进行优先处理,使用机器学习-监督学习或聚类将它们进行分类(新闻,支持,金融,娱乐等)。然后我们可以在"瞬间"中将这些文章作为热门话题显示。
搜索:搜索涉及到推文的索引、排名和检索。我们在下一个问题“设计Twitter搜索(Design Twitter Search)”中讨论了类似的解决方案。
推荐阅读
你好,我是拾叁,7年开发老司机、互联网两年外企5年。怼得过阿三老美,也被PR comments搞崩溃过。这些年我打过工,创过业,接过私活,也混过upwork。赚过钱也亏过钱。一路过来,给我最深的感受就是不管学什么,一定要不断学习。只要你能坚持下来,就很容易实现弯道超车!所以,不要问我现在干什么是否来得及。如果你还没什么方向,可以先关注我,这里会经常分享一些前沿资讯和编程知识,帮你积累弯道超车的资本。